<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue Simulation</title>
</head>
<body>
  <h2>Vue Simulation</h2>
  <hr/>
  <div id="app">
    <div>
      Main Test Part: <br>
      {{msg}},<br>
      {{msg}},<br>
      {{name}}
    </div>
    <button onclick="changeMsg()">Change message => {{text}}</button>
    <hr/>
    <div>{{other}}</div>
    <hr/>
    <span>{{name}}</span>
    <input type="text" v-model="other">
    <hr/>
    <p>{{text}}</p>
  </div>

  <script type="text/javascript">

  /*****************************************************************
      本例是在学习完coderwhy老师的Vue教程之后写的，除了参考coderwhy老师的视频
  外，也参考了Vue本身的代码，再加上自己的一些试验发挥，实现了数据的双向绑定和数据
  对象的劫持与显示。本例只是对SimulateVue.html中的实现作了一些简化，只处理了单
  层数据，相对更简单一些，便于学习理解。

      本代码只供学习使用，作者不承担任何可能的版权及法律风险。
  ******************************************************************/

  const regTag = /\{\{([\w\d\.]+)\}\}/g

    function htmlEscape(src) {
      if ((typeof src) === 'string') {
        return src.replace(/[<>&"]/g, function(c){
          return {'<':'&lt;','>':'&gt;','&':'&amp;','"':'&quot;'}[c]
        })
      }
      else {
        return src
      }
    }    

    class Vue {
      constructor(el, data) {
        this._el = el
        this._data = data

        new Observer(this, data)
        new Compiler(this, el)
      }
    }

    class Observer{
      constructor(vm, data) {
        this._vm = vm
        this._data = data
        this._dep = []
        this._proxySimple(vm)
        this._proxy(this._data, data)
      }

      _proxySimple(root) {
        Object.keys(this._data).forEach((key) => {
          Object.defineProperty(root, key, {
            configurable: true,
            enumerable: true,
            get() {
              return this._data[key]
            },
            set(value) {
              if (this._data[key] === value) return
              this._data[key] = value
            }
          })
        })
      }

      _proxy (parent, data) {
        Object.keys(data).forEach((key) => {
          let d = new Dep()

          let val = parent[key]

          Object.defineProperty(parent, key, {
            configurable: true,
            enumerable: true,
            get () {
              if (Dep.target) {
                d.addWatcher(Dep.target)
              }
              return val //data[key]
            },
            set (newVal) {
              if (val === newVal) return

              val = htmlEscape(newVal)
              d.notify()
            }
          })

          if (val instanceof Object) {
            this._proxy(parent[key], val);
          }
        })
      }
    }

    class Dep {
      constructor() {
        this.watchers = []
      }

      addWatcher(watcher) {
        this.watchers.push(watcher)
      }

      notify() {
        this.watchers.forEach( item => {
          item.update()
        })
      }
    }

    class Watcher {
      constructor(node, name, vm) {
        this.node = node
        this.nodeText = this._getNodeText(node)
        this.name = name
        this._vm  = vm
        Dep.target = this
        this.update()
        Dep.target = null
      }

      _getNodeText(node) {
        return (node.nodeType === 3) ? node.nodeValue : node.innerHTML
      }

      _getRealText(s) {
        if (regTag.test(s)) {
          return s.replace(regTag, ($0, $1, $2) => {
            return this._vm[$1.trim()]
          })
        }
        else {
          return s
        }
      }

      update() {
        let nodeType = this.node.nodeType
        if (nodeType === 1) {
          let a = this.node.getAttribute('v-model')
          if (a) {                  // v-model: two-way binding
            this.node.value = this._vm[a]
          }
          else {                    // one-way binding
            this.node.innerHTML = this._getRealText(this.nodeText)
          }
        }
        else if (nodeType === 3) {  // text node
          this.node.nodeValue = this._getRealText(this.nodeText)
        }
      }
    }

    class Compiler {
      constructor(vm, el) {
        this._vm = vm
        this.elroot = document.querySelector(el);
        if (!this.elroot) {
          console.log('cannot find the elment:', el);
          return;
        }
        let frag = this._createFrag()
        if (frag) {
          this.elroot.appendChild(frag)
        }


      }

      _createFrag(){
        const frag = document.createDocumentFragment();

        let child = null;
        while (child = this.elroot.firstChild) {
          if (this._compile(child)) {
            frag.appendChild(child)
          }
        }

        return (frag.childElementCount) ? frag : null
      }

      _compile(node) {
        let nodeType = node.nodeType
        if (nodeType === 1) {      // element
          let a = node.getAttribute('v-model')
          if (a) {                 // v-model: two-way binding
            a = a.trim()
            node.value = this._vm[a]
            node.addEventListener("input", (e) => {
              this._vm[a] = e.target.value
            })
            new Watcher(node, a, this._vm)
            return this._vm[a]
          }
          else {                   // one-way binding
            let s = node.innerHTML
            if (regTag.test(s)) {
              node.innerHTML = s.replace(regTag, ($0, $1, $2) => {
                new Watcher(node, $1.trim(), this._vm)
                return this._vm[$1.trim()]
              })
            }
          }
        }
        else if (nodeType === 3) { // text node
          let s = node.nodeValue
          if (regTag.test(s)) {
            node.nodeValue = s.replace(regTag, ($0, $1, $2) => {
              new Watcher(node, $1.trim(), this._vm)
              return this._vm[$1.trim()]
            })
          }
        }
        return node
      }
    }

    const data = {
      msg: "Simulation message",
      name: "my Vue",
      other: "other part",
      text: "text node"
    }

    const vm = new Vue('#app', data)

    console.log(vm);

    function randomStr(len) {  
      len = len || 32
      var template = "0123456789   abcdefghijklmnopqrstuvwxyz    ABCDEFGHIJKLMNOPQRSTUVWXYZ"
      tmpLen = template.length
      res = ""
      for (i = 0; i < len; i++)
        res += template.charAt(Math.floor(Math.random() * tmpLen))
      return res
    }

    function randomNum(Min, Max) {
      return Min + Math.round(Math.random() * (Max - Min))
    }

    function changeMsg(){
      vm.msg = randomStr(randomNum(4, 32))
    }

  </script>
</body>
</html>